package org.sonatype.maven.wagon; /******************************************************************************* * Copyright (c) 2010-2011 Sonatype, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.util.Properties; import java.util.concurrent.ExecutionException; import org.apache.maven.wagon.ConnectionException; import org.apache.maven.wagon.InputData; import org.apache.maven.wagon.OutputData; import org.apache.maven.wagon.ResourceDoesNotExistException; import org.apache.maven.wagon.StreamWagon; import org.apache.maven.wagon.TransferFailedException; import org.apache.maven.wagon.authentication.AuthenticationException; import org.apache.maven.wagon.authorization.AuthorizationException; import org.apache.maven.wagon.events.TransferEvent; import org.apache.maven.wagon.proxy.ProxyInfo; import org.apache.maven.wagon.resource.Resource; import org.codehaus.plexus.util.StringUtils; import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder; import com.ning.http.client.AsyncHttpClientConfig; import com.ning.http.client.ProxyServer; import com.ning.http.client.Realm; import com.ning.http.client.Realm.RealmBuilder; import com.ning.http.client.Response; import com.ning.http.client.providers.netty.NettyAsyncHttpProvider; public class AhcWagon extends StreamWagon { /** * @plexus.configuration default="false" */ private boolean useCache; /** * @plexus.configuration default="10" */ private int maxRedirections = 10; /** * @plexus.configuration */ private Properties httpHeaders; /** * Encoding to use to send credentials to server. RFC 2617 doesn't specify which but for interop with the JRE's HTTP * client as well as Jetty, we default to Latin-1. * * @plexus.configuration */ private String credentialEncoding = "ISO-8859-1"; private AsyncHttpClient httpClient; @Override protected void openConnectionInternal() throws ConnectionException, AuthenticationException { String protocol = UrlUtils.getProtocol( getRepository().getUrl() ); protocol = UrlUtils.normalizeProtocol( protocol ); AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder(); RealmBuilder realmBuilder = null; if ( authenticationInfo != null ) { String username = authenticationInfo.getUserName(); String password = authenticationInfo.getPassword(); realmBuilder = (new Realm.RealmBuilder()) .setPrincipal( username ) .setPassword( password ) .setUsePreemptiveAuth( false ) .setEnconding( credentialEncoding ); builder.setRealm( realmBuilder.build() ); } ProxyInfo proxyInfo = getProxyInfo( protocol, getRepository().getHost() ); if ( proxyInfo == null && "https".equalsIgnoreCase( protocol ) ) { proxyInfo = getProxyInfo( "http", getRepository().getHost() ); } if ( proxyInfo != null ) { String proxyUsername = proxyInfo.getUserName(); String proxyPassword = proxyInfo.getPassword(); String proxyHost = proxyInfo.getHost(); int proxyPort = proxyInfo.getPort(); String proxyNtlmHost = proxyInfo.getNtlmHost(); String proxyNtlmDomain = proxyInfo.getNtlmDomain(); if (StringUtils.isNotEmpty( proxyNtlmHost )) { proxyHost = proxyNtlmDomain; } if ( proxyHost != null ) { ProxyServer proxyServer = new ProxyServer( proxyHost, proxyPort ); if ( proxyUsername != null && proxyPassword != null ) { proxyServer = new ProxyServer( proxyHost, proxyPort, proxyUsername, proxyPassword ); proxyServer.setEncoding( credentialEncoding ); } else { proxyServer = new ProxyServer( proxyHost, proxyPort ); } if ( StringUtils.isNotEmpty( proxyNtlmDomain ) ) { proxyServer.setNtlmDomain(proxyNtlmDomain); } builder.setProxyServer( proxyServer ); } } builder.setConnectionTimeoutInMs( getTimeout() ); builder.setRequestTimeoutInMs( getTimeout() ); builder.setFollowRedirects( maxRedirections > 0 ); builder.setMaximumNumberOfRedirects( maxRedirections ); builder.setUserAgent( "Apache-Maven" ); builder.setCompressionEnabled( true ); if ( httpHeaders != null && httpHeaders.getProperty( "User-Agent" ) != null ) { builder.setUserAgent( httpHeaders.getProperty( "User-Agent" ) ); } AsyncHttpClientConfig config = builder.build(); // NOTE: Explicitly specify provider to workaround class loading bug in ahc:1.4.0 httpClient = new AsyncHttpClient( new NettyAsyncHttpProvider( config ), config ); } @Override public void closeConnection() throws ConnectionException { if ( httpClient != null ) { httpClient.close(); httpClient = null; } } public void setHttpHeaders( Properties properties ) { if ( properties != null ) { httpHeaders = new Properties(); httpHeaders.putAll( properties ); } else { httpHeaders = null; } } private void addHeaders( BoundRequestBuilder builder ) { builder.addHeader( "Accept-Encoding", "gzip" ); if ( !useCache ) { builder.addHeader( "Pragma", "no-cache" ); builder.addHeader( "Cache-Control", "no-cache, no-store" ); } if ( httpHeaders != null ) { for ( Object key : httpHeaders.keySet() ) { builder.addHeader( key.toString(), httpHeaders.getProperty( key.toString() ) ); } } } @Override public boolean resourceExists( String resourceName ) throws TransferFailedException, AuthorizationException { try { String url = UrlUtils.buildUrl( getRepository().getUrl(), resourceName ); BoundRequestBuilder builder = httpClient.prepareHead( url ); addHeaders( builder ); Response response = builder.execute().get(); int statusCode = response.getStatusCode(); switch ( statusCode ) { case HttpURLConnection.HTTP_OK: return true; case HttpURLConnection.HTTP_NOT_FOUND: return false; case HttpURLConnection.HTTP_UNAUTHORIZED: case HttpURLConnection.HTTP_FORBIDDEN: throw new AuthorizationException( "Access denied to: " + url + " (" + statusCode + ")" ); default: throw new TransferFailedException( "Failed to check for existence of resource " + url + " (" + statusCode + ")" ); } } catch ( URISyntaxException e ) { return false; } catch ( IOException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } catch ( InterruptedException e ) { throw new TransferFailedException( "Transfer was aborted by client: " + e.getMessage(), e ); } catch ( ExecutionException e ) { throw new TransferFailedException( "Transfer was aborted by client: " + e.getMessage(), e ); } catch ( RuntimeException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } } @Override public void fillInputData( InputData inputData ) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { Resource resource = inputData.getResource(); try { String url = UrlUtils.buildUrl( getRepository().getUrl(), resource.getName() ); BoundRequestBuilder builder = httpClient.prepareGet( url ); builder.setFollowRedirects( true ); addHeaders( builder ); GetExchange exchange = new GetExchange( httpClient ); builder.execute( new GetExchangeHandler( exchange ) ); exchange.await(); if ( exchange.getError() != null ) { throw (IOException) new IOException( exchange.getError().getMessage() ).initCause( exchange.getError() ); } int statusCode = exchange.getStatusCode(); switch ( statusCode ) { case HttpURLConnection.HTTP_OK: case HttpURLConnection.HTTP_NOT_MODIFIED: break; case HttpURLConnection.HTTP_UNAUTHORIZED: case HttpURLConnection.HTTP_FORBIDDEN: throw new AuthorizationException( "Access denied to: " + url + " (" + statusCode + ")" ); case HttpURLConnection.HTTP_NOT_FOUND: throw new ResourceDoesNotExistException( "Unable to locate resource in repository" ); default: throw new TransferFailedException( "Error transferring file, server returned status code " + statusCode ); } inputData.setInputStream( exchange.getInputStream() ); resource.setLastModified( exchange.getLastModified() ); resource.setContentLength( exchange.getContentLength() ); } catch ( URISyntaxException e ) { throw new ResourceDoesNotExistException( "Invalid repository URL", e ); } catch ( IOException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } catch ( InterruptedException e ) { throw new TransferFailedException( "Transfer was aborted by client: " + e.getMessage(), e ); } } @Override public void fillOutputData( OutputData outputData ) throws TransferFailedException { Resource resource = outputData.getResource(); try { String url = UrlUtils.buildUrl( getRepository().getUrl(), resource.getName() ); BoundRequestBuilder builder = httpClient.preparePut( url ); addHeaders( builder ); PutOutputStream pos = new PutOutputStream( builder, url, resource.getContentLength() ); outputData.setOutputStream( pos ); } catch ( URISyntaxException e ) { throw new TransferFailedException( "Invalid repository URL", e ); } } @Override protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { PutOutputStream pos = (PutOutputStream) output; try { Response response = pos.send(); handleStatusCode( response.getStatusCode(), pos.getUrl() ); } catch ( IOException e ) { fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); throw new TransferFailedException( "Error transferring file", e ); } } public void put( File source, String resourceName ) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { Resource resource = new Resource( resourceName ); firePutInitiated( resource, source ); resource.setContentLength( source.length() ); resource.setLastModified( source.lastModified() ); try { String url = UrlUtils.buildUrl( getRepository().getUrl(), resource.getName() ); BoundRequestBuilder builder = httpClient.preparePut( url ); addHeaders( builder ); builder.setBody( new ProgressingFileBodyGenerator( source, resource, this ) ); Response response = builder.execute().get(); handleStatusCode( response.getStatusCode(), url ); firePutCompleted( resource, source ); } catch ( URISyntaxException e ) { throw new TransferFailedException( "Invalid repository URL", e ); } catch ( IOException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } catch ( InterruptedException e ) { throw new TransferFailedException( "Transfer was aborted by client: " + e.getMessage(), e ); } catch ( ExecutionException e ) { throw new TransferFailedException( "Transfer was aborted by client: " + e.getMessage(), e ); } catch ( RuntimeException e ) { throw new TransferFailedException( "Error transferring file: " + e.getMessage(), e ); } } private void handleStatusCode( int statusCode, String url ) throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { switch ( statusCode ) { case HttpURLConnection.HTTP_OK: case HttpURLConnection.HTTP_CREATED: case HttpURLConnection.HTTP_ACCEPTED: case HttpURLConnection.HTTP_NO_CONTENT: break; case HttpURLConnection.HTTP_UNAUTHORIZED: case HttpURLConnection.HTTP_FORBIDDEN: throw new AuthorizationException( "Access denied to: " + url + " (" + statusCode + ")" ); case HttpURLConnection.HTTP_NOT_FOUND: throw new ResourceDoesNotExistException( "File " + url + " does not exist" ); default: throw new TransferFailedException( "Failed to transfer file " + url + ". Return code is: " + statusCode ); } } void firePutStarted( File source, Resource resource ) { firePutStarted( resource, source ); } void fireTransferProgressed( TransferEvent event, int count, byte[] buffer ) { fireTransferProgress( event, buffer, count ); } }